﻿using ARSoft.Tools.Net.Dns;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Linq;
using System.Net.Mail;
using System;
using ResponseType = DCode.EmailValidator.Core.Data.ResponseType;
using ValidatorResult = DCode.EmailValidator.Core.Data.ValidatorResult;
using DCode.EmailValidator.Core.Cloud;

namespace DCode.EmailValidator.Core
{
    public class Validator : IDisposable
    {
        private string _mailFrom = "default@domain.com";
        private int _smtpPort = 25;
        const string Pattern = "^([0-9a-zA-Z]([-\\.\\w]*[0-9a-zA-Z])*@([0-9a-zA-Z][-\\w]*[0-9a-zA-Z]\\.)+[a-zA-Z]{2,9})$";
        DateTime _dteTimeOutCheck;
        private const int MvarSmtpTimeOut = 20;

        #region Properties
        public string MailFrom
        {
            get
            {
                return _mailFrom;
            }
            set
            {
                _mailFrom = value;
            }
        }

        public int SmtpPort
        {
            get
            {
                return _smtpPort;
            }
            set
            {
                _smtpPort = value;
            }
        }
        #endregion

        public void Dispose()
        {
        }
        
        /// <summary>
        /// Get the domain name (ej: gmail.com) from an email address
        /// </summary>
        /// <param name="mailAddress">The email address to check</param>
        /// <returns>The domain name</returns>
        private string GetDomainName(string mailAddress)
        {
            return !mailAddress.Contains("@") ? mailAddress : mailAddress.Split('@')[1];
        }

        /// <summary>
        /// Fix the MX records of the domain to make the validation
        /// </summary>
        /// <param name="emailOrDomain">The email address or domain</param>
        /// <returns>The list of MX records</returns>
        private string[] FindMxRecords(string emailOrDomain)
        {
            var response = DnsClient.Default.Resolve(emailOrDomain, RecordType.Mx);
            var records = response.AnswerRecords.OfType<MxRecord>();
            
            //return records.Select(record => record.ExchangeDomainName).ToArray();
            return records.OrderBy(c => c.Preference).Select(record => record.ExchangeDomainName).ToArray();
        }

        /// <summary>
        /// Get a response of a message sended over a socket
        /// </summary>
        /// <param name="socket">The socket to receive the info</param>
        /// <returns>A string with server response</returns>
        private string GetResponse(Socket socket)
        {
            var numArray = new byte[1024];
            var num = 0;
            while (socket.Available == 0)
            {
                Thread.Sleep(100);
                ++num;
                if (num <= 30) continue;
                socket.Close();

                return "Timeout";
            }

            socket.Receive(numArray, 0, socket.Available, SocketFlags.None);
            var @string = Encoding.ASCII.GetString(numArray);

            return @string.IndexOf(Environment.NewLine, StringComparison.Ordinal) > 0 ? @string.Substring(0, @string.IndexOf(Environment.NewLine, StringComparison.Ordinal)) : @string;
        }
        
        /// <summary>
        /// Send data over a socket to specific server
        /// </summary>
        /// <param name="socket">The socket to send the info</param>
        /// <param name="msg">The message</param>
        private void SendData(Socket socket, string msg, ICollection<string> actions)
        {
            actions.Add(msg);

            var bytes = Encoding.ASCII.GetBytes(msg);
            socket.Send(bytes, 0, bytes.Length, SocketFlags.None);
        }

        /// <summary>
        /// Check the email format
        /// </summary>
        /// <param name="mailAdress">Email for validation</param>
        /// <returns></returns>
        private bool CheckSyntax(string mailAdress)
        {
            return new Regex(Pattern).IsMatch(mailAdress);
        }

        /// <summary>
        /// Validate a domain name
        /// </summary>
        /// <param name="emailOrDomain">The domain name for validation</param>
        /// <returns>Bool with validation result</returns>
        public bool CheckDomainName(string emailOrDomain)
        {
            var domainName = GetDomainName(emailOrDomain);

            try
            {
                Dns.GetHostEntry(domainName);
            }
            catch (Exception)
            {
                return false;
            }
            return true;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="emailFrom"></param>
        /// <param name="emailAddress"></param>
        /// <returns></returns>
        public ValidatorResult ValidateMail(string emailFrom, string emailAddress)
        {
            _mailFrom = emailFrom;
            return ValidateMail(emailAddress);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="emailAddress"></param>
        /// <returns></returns>
        public ValidatorResult ValidateMail(string emailAddress)
        {
            var result = new ValidatorResult();

            var actions = new List<string>();

            try
            {
                if (!emailAddress.Contains("@") || !CheckSyntax(emailAddress))
                {
                    actions.Add("Format validation: " + emailAddress + " NOK");
                    result.ResultType = ResponseType.InvalidSintax; //Invalid email syntax;
                    result.IsValid = false;

                    return result;
                }

                actions.Add("Format validation: " + emailAddress + " OK");

                if (!Regex.IsMatch(emailAddress, Pattern))
                {
                    actions.Add("Format validation: " + emailAddress + " NOK");
                    result.ResultType = ResponseType.InvalidSintax; //Invalid email syntax;
                    result.IsValid = false;
                }

                var mail = new MailAddress(emailAddress);

                var mxRecords = FindMxRecords(mail.Host);

                if (mxRecords.Length == 0)
                {
                    actions.Add("Domain validation: " + mail.Host + " NOK");
                    result.ResultType = ResponseType.NoMxRecords; // No MX records for the specified domain;
                    result.IsValid = false;
                }

                actions.Add("Domain validation: " + mail.Host + " OK");

                foreach (var hostNameOrAddress in mxRecords)
                {
                    try
                    {
                        var ipEndPoint = new IPEndPoint(Dns.GetHostEntry(hostNameOrAddress).AddressList[0], _smtpPort);
                        var s = new Socket(ipEndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
                        s.Connect(ipEndPoint);

                        actions.Add("Connecting to: " + hostNameOrAddress);

                        var str = GetResponse(s);

                        if (Convert.ToInt32(str.Substring(0, 3)) != (int) ResponseType.Ok) continue;

                        SendData(s, string.Format("HELO {0}\r\n", mail.Host), actions);

                        if (!WaitForResponse(s, ResponseType.Connected, actions))
                        {
                            result.ResultType = ResponseType.NotConnected;
                            result.IsValid = false;
                            result.Actions = actions.ToArray();

                            return result;
                        }

                        SendData(s, string.Format("MAIL FROM:<{0}>\r\n", _mailFrom), actions);

                        if (!WaitForResponse(s, ResponseType.Connected, actions))
                        {
                            result.ResultType = ResponseType.InvalidMail;
                            result.IsValid = false;
                            result.Actions = actions.ToArray();

                            return result;
                        }

                        SendData(s, string.Format("RCPT TO:<{0}>\r\n", emailAddress), actions);

                        if (!WaitForResponse(s, ResponseType.Connected, actions))
                        {
                            result.ResultType = ResponseType.InvalidMail;
                            result.IsValid = false;
                            result.Actions = actions.ToArray();

                            return result;
                        }

                        if (emailAddress.ToLower().Contains("yahoo"))
                        {
                            SendData(s, "DATA\r\n", actions);

                            if (!WaitForResponse(s, ResponseType.DataOk, actions))
                            {
                                result.ResultType = ResponseType.InvalidMail;
                                result.IsValid = false;
                                result.Actions = actions.ToArray();

                                return result;
                            }

                            var strSmtpData = "";
                            strSmtpData = "From:" + _mailFrom + "<" + _mailFrom + ">" + Environment.NewLine;
                            strSmtpData += "To:" + emailAddress + "<" + emailAddress + ">" + Environment.NewLine;
                            strSmtpData += "Subject: Email Verification " + Environment.NewLine + Environment.NewLine;
                            strSmtpData += " " + Environment.NewLine + "." + Environment.NewLine;

                            SendData(s, strSmtpData, actions);

                            if (!WaitForResponse(s, ResponseType.Connected, actions))
                            {
                                result.ResultType = ResponseType.InvalidMail;
                                result.IsValid = false;
                                result.Actions = actions.ToArray();

                                return result;
                            }
                        }

                        SendData(s, "QUIT\r\n", actions);

                        str = GetResponse(s);
                        actions.Add(str);
                        
                        s.Close();

                        result.ResultType = ResponseType.Ok;
                        result.IsValid = true;
                        result.Actions = actions.ToArray();

                        return result;
                    }
                    catch (Exception ex)
                    {
                        result.ResultType = ResponseType.MxRecordsProblem;
                        result.IsValid = false;
                        result.Message = ex.Message;
                        result.Actions = actions.ToArray();

                        return result;
                    }
                }

                if (actions.Count > 0)
                    result.Actions = actions.ToArray();

                return result;
            }
            catch (Exception ex)
            {
                result.ResultType = ResponseType.Error;
                result.IsValid = false;

                actions.Add(ex.Message);

                result.Actions = actions.ToArray();

                return result;
            }
        }

        public ValidatorResult MakeCloudValidation(string login, string password,string fromEmail, string emailAddress)
        {
            try
            {
                var service = new EmailService();

                var response = service.ValidateAddress(login, password,fromEmail, emailAddress);

                return new ValidatorResult()
                {
                    Actions = response.Actions,
                    IsValid = response.IsValid,
                    Message = response.Message,
                    ResultType = (ResponseType)Enum.Parse(typeof(ResponseType), response.ResultType.GetHashCode().ToString())
                };
            }
            catch (Exception)
            {
                return new ValidatorResult() { IsValid = false, Message = "Error contacting services, please retry later" };
            }
        }

        private bool WaitForResponse(Socket socket, ResponseType responseType, ICollection<string> actions)
        {
            var strResponseCode = responseType.GetHashCode().ToString();

            _dteTimeOutCheck = DateTime.Now;
            var tsp = DateTime.Now - _dteTimeOutCheck;
            var numArray = new byte[1024];

            while (tsp.Seconds < MvarSmtpTimeOut)
            {
                if (socket.Available > 0)
                {
                    socket.Receive(numArray, 0, socket.Available, SocketFlags.None);
                    var @string = Encoding.ASCII.GetString(numArray).Replace("\0","");

                    actions.Add(@string);

                    if (@string.Substring(0, strResponseCode.Length) == strResponseCode)
                        return true;
                }

                tsp = DateTime.Now - _dteTimeOutCheck;
            }

            return false;
        }
    }
}
